﻿import mx.events.EventDispatcher;
import mx.utils.Delegate;

import org.molamil.acto.core.EventConstants;
import org.molamil.acto.core.Logger;
import org.molamil.acto.mc.mcw.LcMcWrapper;
import org.molamil.acto.mc.mcw.McwInit;
import org.molamil.acto.pages.AbstractMovieContext;
import org.molamil.acto.pages.MovieBuilder;
import org.molamil.acto.pages.MovieContext;
import org.molamil.acto.pages.MovieRequest;
import org.molamil.acto.pages.PageBuilder;
import org.molamil.acto.pages.PageContext;
import org.molamil.acto.pages.PageRequest;
import org.molamil.acto.pages.PagesConf;
import org.molamil.acto.pages.controller.MovieNotFoundError;
import org.molamil.acto.pages.controller.PagesController;
import org.molamil.acto.pages.factory.AbstractPagesFactory;
import org.molamil.acto.pages.factory.MovieContainer;
import org.molamil.acto.pages.factory.PageContainer;
import org.molamil.acto.pages.factory.PageNotFoundError;
import org.molamil.acto.pages.history.BasicHistoryManager;
import org.molamil.acto.pages.history.HistoryStack;
import org.molamil.acto.pages.master.AbstractMovieMaster;
import org.molamil.acto.pages.master.ExternalMovieMaster;
import org.molamil.acto.pages.master.ExternalPageMaster;
import org.molamil.acto.pages.master.Master;
import org.molamil.acto.pages.master.MovieMaster;
import org.molamil.acto.pages.master.PageMaster;
import org.molamil.commons.EventUtil;
import org.molamil.commons.StringUtil;
import org.molamil.acto.core.AppRole;

/**
 * Functional <code>PageController</code> implementation.<br />
 * Dispatches:
 * <ul>
 * 	<li><code>onMcStateChange</code>: The <code>state</code> of the MovieClip wrapper has changed.</li>
 * </ul>
 *
 * @author Jorge Hernandez
 */
class org.molamil.acto.pages.controller.AbstractPagesController implements PagesController, 
																		   PageContainer, MovieContainer {
	
	
	// Constants
	
	public static var ERROR_PAGE_NOT_FOUND:String = "ERROR: Page not found: \"{page}\".";
	public static var ERROR_MOVIE_NOT_FOUND:String = "ERROR: Movie not found: \"{movie}\".";
	
	
	// Properties
	
	private static var log:Logger = Logger.getLogger("AbstractPagesController");
	
	private var factory:AbstractPagesFactory;
	private var currentPage:PageMaster;	private var displayedPage:PageMaster;
	private var nextPage:PageMaster;
	private var historyManager:BasicHistoryManager;
	private var displayedMovies:Array;
	
	private var mcStateChangeDelegate:Function;
	private var pageLoadErrorDelegate:Function;
	
	
	// Constructor
	
	public function AbstractPagesController() {
		log.debug("Constructor");
		EventDispatcher.initialize(this);
		McwInit.init();
		historyManager = new BasicHistoryManager(this);
		EventUtil.propagate(historyManager, this, EventConstants.HISTORY_BACK);		EventUtil.propagate(historyManager, this, EventConstants.HISTORY_FORWARD);
		displayedMovies = new Array();
		mcStateChangeDelegate = Delegate.create(this, mcStateChangeListener);
		pageLoadErrorDelegate = Delegate.create(this, pageLoadErrorListener);
	}
	
	
	// Functions
	
	public function dispatchEvent(eventObj:Object):Void {}
	public function addEventListener(event:String, handler):Void {}
	public function removeEventListener(event:String, handler):Void {}
	
	public function getPage(name:String):PageContext {
		return factory.getPage(name);
	}
	
	public function getPages():Array {
		return factory.getPages();
	}
	
	public function getPagesAt(level:Number):Array {
		return factory.getPagesAt(level);
	}
	
	public function containsPage(name:String):Boolean {
		return factory.containsPage(name);
	}
	
	public function getMovie(name:String):MovieContext {
		return factory.getMovie(name);
	}
	
	public function getMovies():Array {
		return factory.getMovies();
	}
	
	public function containsMovie(name:String):Boolean {
		return factory.containsMovie(name);
	}
	
	public function goto(name:String, params:Object):PageMaster {
		
		log.info("goto(), name = " + name);
		log.debug("currentPage name = " + currentPage.getContext().name);
		
		// Get the page.
		var pageContext:PageContext = factory.getPage(name);
		
		// Check if it's an alias page.
		if (pageContext.alias != null) {
			log.info("alias page: " + pageContext.alias);
			return goto(pageContext.alias, params);
		}
		
		// Create the master.
		var page:PageMaster = null;
		
		// Check whether we are already in that page and wether that page is already being requested.
		if (name == currentPage.getContext().name && params["_isReload"] != true) {
			
			log.info("no page to goto - we are already in " + name);
			
		} else if (name == nextPage.getContext().name) {
			
			log.info("no page to goto - we the page is already being requested " + name);
			
		} else {
			
			// Get the requested context.
			if (pageContext == null) {
				var msg:String = StringUtil.replace(ERROR_PAGE_NOT_FOUND, "{page}", name);
				throw new PageNotFoundError(msg);	
			}
			
			// Throw page request event.
			var eventObject:Object = {target: pageContext, type: EventConstants.PAGE_REQUEST};
			dispatchEvent(eventObject);
			
			// Check the role.
			if (pageContext.role != null && pageContext.role != "" && pageContext.role != AppRole.getInstance().currentRole.name) {
				log.warning("invalid role, required: " + pageContext.role);
				log.warning("invalid role, found: " + AppRole.getInstance().currentRole.name);
				var eventObject:Object = {target: pageContext, type: EventConstants.ROLE_REQUIRED_ERROR, name: name};
				dispatchEvent(eventObject);
				return;
			}
				
			// Build the page
			page = PageBuilder.create(pageContext);
				
			// Create the request.
			var request:PageRequest = new PageRequest();
			request.name = name;
			request.params = params;
			request.referrer = currentPage;
			AbstractMovieMaster(page).request = request;
			
			// Set the current page.
			currentPage = page;
			
			// Add the depends.
			for (var i:Number = 0; i < pageContext.depends.length; i++)
				AbstractMovieMaster(page).addDepends(pageContext.depends[i]);
				
			// Add the listeners to the requested page.
			EventUtil.propagate(page, this, EventConstants.PAGE_DISPLAY);
			EventUtil.propagate(page, this, EventConstants.PAGE_CLEAR);
			EventUtil.propagate(page, this, EventConstants.PAGE_IN);
			EventUtil.propagate(page, this, EventConstants.PAGE_ON);
			EventUtil.propagate(page, this, EventConstants.PAGE_OUT);
			EventUtil.propagate(page, this, EventConstants.PAGE_OFF);
			if (page instanceof ExternalPageMaster) {
				EventUtil.propagate(page, this, EventConstants.PAGE_LOAD_START);
				EventUtil.propagate(page, this, EventConstants.PAGE_LOAD_PROGRESS);
				EventUtil.propagate(page, this, EventConstants.PAGE_LOAD_INIT);
				EventUtil.propagate(page, this, EventConstants.PAGE_LOAD_ERROR);
			}
			AbstractMovieMaster(page).addEventListener(EventConstants.PAGE_LOAD_ERROR, pageLoadErrorDelegate);
			AbstractMovieMaster(page).addEventListener(EventConstants.MC_STATE_CHANGE, mcStateChangeDelegate);
			
			// Clear the current page.
			if (displayedPage != null)
				displayedPage.clear();
			
			// Show and register the requested page.
			if (displayedPage != null && displayedPage.isDisplayed() && PagesConf.isDisplayAtOff) {
				log.debug("displaying at off");
				nextPage = page;
			} else {
				page.display();
				displayedPage = page;
				displayDepends(page);
				nextPage = null;
			}
			
		}
		
		// Check if it's a redirect page.
		if (pageContext.redirect != null) {
			log.info("redirect page: " + pageContext.alias);
			goto(pageContext.redirect, params);
		}
		
		return page;
		
	}
	
	public function reloadPage():Void {
		
		var name:String = currentPage.getContext().name;
		var params:Object = currentPage.getRequest().params;
		
		if (params == null)
			params = new Object();
		params["_isReload"] = true;
		
		log.info("reloadPage(), name = " + name);
		
		goto(name, params);
		
	}
	
	public function getDisplayedPage():PageMaster {
		log.info("getDisplayedPage(): " + displayedPage.getContext().name);
		return displayedPage;
	}
	
	function getCurrentPage():PageMaster {
		log.info("getCurrentPage(): " + currentPage.getContext().name);
		return currentPage;
	}
	
	public function displayMovie(name:String, params:Object):MovieMaster {
		
		log.info("displayMovie(), name = " + name);
		
		// Create the master.
		var movie:MovieMaster = getDisplayedMovie(name);
		
		// Check whether it's already displayed.
		if (movie != null) {
			
			// If it's a depends movie, add the caller.
			if (params[PagesConf.MOVIE_PARAM_CALLER] != null) {
				movie.addCaller(params[PagesConf.MOVIE_PARAM_CALLER]);
			} else {
				movie.addCaller(movie.getContext().name);
			}
			
			log.info("no movie to display - the movie is already displayed: " + name);
			
		} else {
		
			// Get the movie.
			var movieContext:MovieContext = factory.getMovie(name);
			
			// Throw page request event.
			var eventObject:Object = {target: movieContext, type: EventConstants.MOVIE_REQUEST};
			dispatchEvent(eventObject);
				
			// Get the requested context.
			if (movieContext == null) {
				var msg:String = StringUtil.replace(ERROR_MOVIE_NOT_FOUND, "{movie}", name);
				throw new MovieNotFoundError(msg);	
			}
					
			// Build the movie
			movie = MovieBuilder.create(movieContext);
				
			// Create the request.
			var request:MovieRequest = new MovieRequest();
			request.name = name;
			request.params = params;
			AbstractMovieMaster(movie).request = request;
			
			// Add the depends.
			for (var i:Number = 0; i < movieContext.depends.length; i++)
				AbstractMovieMaster(movie).addDepends(movieContext.depends[i]);
			
			// Add the caller.
			if (params[PagesConf.MOVIE_PARAM_CALLER] != null) {
				MovieRequest(movie.getRequest()).caller = params[PagesConf.MOVIE_PARAM_CALLER];
			} else {
				MovieRequest(movie.getRequest()).caller = movie.getContext().name;
			}
			movie.addCaller(MovieRequest(movie.getRequest()).caller);
			
			// Add the listeners to the requested moive.
			EventUtil.propagate(movie, this, EventConstants.MOVIE_DISPLAY);
			EventUtil.propagate(movie, this, EventConstants.MOVIE_CLEAR);
			EventUtil.propagate(movie, this, EventConstants.MOVIE_IN);
			EventUtil.propagate(movie, this, EventConstants.MOVIE_ON);
			EventUtil.propagate(movie, this, EventConstants.MOVIE_OUT);
			EventUtil.propagate(movie, this, EventConstants.MOVIE_OFF);
			EventUtil.propagate(movie, this, EventConstants.MC_STATE_CHANGE);
			if (movie instanceof ExternalMovieMaster) {
				EventUtil.propagate(movie, this, EventConstants.MOVIE_LOAD_START);
				EventUtil.propagate(movie, this, EventConstants.MOVIE_LOAD_PROGRESS);
				EventUtil.propagate(movie, this, EventConstants.MOVIE_LOAD_INIT);
				EventUtil.propagate(movie, this, EventConstants.MOVIE_LOAD_ERROR);
			}
				
			// Show the requested movie.
			movie.display();
			
			// Add the movie to the displayed list.
			addDisplayedMovie(movie);
			
			// Show depends movies.
			displayDepends(movie);
			
		}
		
		return movie;
	
	}
	
	public function displayMovieForPage(name:String, params:Object):MovieMaster {
		
		log.info("displayMovieForPage(), name = " + name);
		
		if (params == null)
			params = new Object();
		params[PagesConf.MOVIE_PARAM_CALLER] = displayedPage.getContext().name;
		
		AbstractMovieMaster(displayedPage).addDepends(name);
			
		return displayMovie(name, params);
		
	}
	
	public function clearMovie(name:String):Void {
		
		log.info("clearMovie(), name = " + name);
		
		var master:MovieMaster = getDisplayedMovie(name);		master.clear();
		removeDisplayedMovie(name);
				clearDepends(master);
	
	}
	
	public function getDisplayedMovie(name:String):MovieMaster {
		var master:MovieMaster = null;
		for (var i:Number = 0; i < displayedMovies.length; i++) {
			var m:MovieMaster = displayedMovies[i];
			if (m.getContext().name == name) {
				master = m;
				break;
			}
		}
		return master;
	}
	
	public function getDisplayedMovies():Array {
		return displayedMovies;
	}
	
	public function isDisplayedMovie(name:String):Boolean {
		return getDisplayedMovie(name) != null;
	}
	
	public function back():PageMaster {
		return historyManager.back();
	}
	
	public function forward():PageMaster {
		return historyManager.forward();
	}
	
	public function getHistory():HistoryStack {
		return historyManager.getHistory();
	}
	
	private function addDisplayedMovie(master:MovieMaster):Void {
		
		log.debug("addDisplayedMovie(), name = " + master.getContext().name);
		
		if (getDisplayedMovie(master.getContext().name) == null)
			displayedMovies.push(master);
		
	}
	
	private function replaceDisplayedMovie(name:String, master:MovieMaster):Void {
		
		log.debug("replaceDisplayedMovie(), name = " + name);
		
		for (var i:Number = 0; i < displayedMovies.length; i++) {
			var m:MovieMaster = displayedMovies[i];
			if (m.getContext().name == name) {
				displayedMovies[i] = master;
				break;
			}
		}
		
	}
	
	private function removeDisplayedMovie(name:String):Void {
		
		log.debug("removeDisplayedMovie(), name = " + name);
		
		var a:Array = new Array();
		for (var i:Number = 0; i < displayedMovies.length; i++) {
			var m:MovieMaster = displayedMovies[i];
			if (m.getContext().name != name)
				a.push(m);
		}
		displayedMovies = a;
		
	}
	
	private function displayDepends(master:Master):Void {
		
		if (master == null)
			return;
		 
		// Get the depends for the current request.
		var a:Array = getFlattenedDepends(master);
		
		// Display the movies.
		for (var i:Number = 0; i < a.length; i++) {
			var params:Object = new Object();
			params[PagesConf.MOVIE_PARAM_CALLER] = master.getContext().name;
			displayMovie(a[i], params);
		}
		
		// If the master is a PageMaster, remove the previous page's obsolete depends.
		if (master instanceof PageMaster) {
			var prevPage:PageMaster = PageRequest(master.getRequest()).referrer;
			if (prevPage != null)
				clearDepends(prevPage);
		}
		
	}
	
	private function clearDepends(master:Master):Void {
		
		// Get the flattened depends.
		var pa:Array = getFlattenedDepends(master);
			
		// Iterate the depends and remove the previous page as a caller.
		// If there's no callers left, remove the depends.
		for (var i:Number = 0; i < pa.length; i++) {
			var d:MovieMaster = getDisplayedMovie(pa[i]);
			d.removeCaller(master.getContext().name);
			if (d.getCallers().length == 0)
				clearMovie(d.getContext().name);
		}
		
	}
	
	private function getFlattenedDepends(o:Object, aInit:Array):Array {
		
		// Get the depends.
		var depends:Array;
		if (o instanceof AbstractMovieContext) {
			depends = AbstractMovieContext(o).depends;
		} else if (o instanceof AbstractMovieMaster) {
			depends = AbstractMovieMaster(o).getDepends();
		} else {
			return;
		}
		
		// Create the array containing depends names or set it to an initial one.
		var a:Array = aInit != null ? aInit : new Array();
		
		// Iterate through all depends in this context.
		for (var i:Number = 0; i < depends.length; i++) {
			
			// Get the name.
			var name:String = depends[i];
			
			// If it's already in the array, continue.
			var isInA:Boolean = false;
			for (var j:Number = 0; j < a.length; j++) {
				if (name == a[j]) {
					isInA = true;
					break;
				}
			}
			if (isInA)
				continue;
			
			// Add the depends and subdepends to the array.
			a.push(name);
			var c:MovieContext = getMovie(name);
			getFlattenedDepends(c, a);
			
		}
		
		return a;
		
	}
	
	private function mcStateChangeListener(eventObject:Object):Void {
		
		log.debug("mcStateChangeListener(), propagating state change");
		log.debug("target name: " + eventObject.target.getContext().name);
		log.debug("state: " + eventObject.state);
		
		// Show the new page.
		if (eventObject.state == LcMcWrapper.STATE_OFF) {
			log.debug("displaying the requested page");
			eventObject.target.request = null;
			displayedPage = nextPage;
			nextPage = null;
			displayedPage.display();
			displayDepends(displayedPage);
		}
		
		// Propagate the event.
		var eventObject:Object = {target: eventObject.target, type: EventConstants.MC_STATE_CHANGE, 
								  state: eventObject.state};
		dispatchEvent(eventObject);
		
	}
	
	private function pageLoadErrorListener(eventObject:Object):Void {
		currentPage = null;		displayedPage = null;
	}
	
	
}